/*
 * Copyright (c) 2021, 2025 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.microprofile.scheduling;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import io.helidon.microprofile.testing.AddBean;
import io.helidon.microprofile.testing.AddExtension;
import io.helidon.microprofile.testing.Configuration;
import io.helidon.microprofile.testing.DisableDiscovery;
import io.helidon.microprofile.testing.junit5.HelidonTest;
import io.helidon.scheduling.CronInvocation;
import io.helidon.scheduling.FixedRateInvocation;
import io.helidon.scheduling.Scheduling;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertFalse;

@HelidonTest
@DisableDiscovery
@AddBean(SchedulingTest.ScheduledBean.class)
@AddBean(SchedulingTest.ScheduledVetoMeBean.class)
@AddExtension(SchedulingCdiExtension.class)
@AddExtension(TestVetoingCdiExtension.class)
@Configuration(configSources = "test.properties")
public class SchedulingTest {

    static final long TWO_SEC_MILLIS = 2 * 1000L;

    final CountDownLatch fixedRateLatch = new CountDownLatch(5);
    final CountDownLatch fixedRateFromConfigLatch = new CountDownLatch(2);
    final CountDownLatch exprLatch = new CountDownLatch(1);
    final CompletableFuture<Integer> noConcurrentExecFuture = new CompletableFuture<>();
    final CompletableFuture<Integer> concurrentExecFuture = new CompletableFuture<>();
    final CompletableFuture<String> overriddenCronFuture = new CompletableFuture<>();
    final CompletableFuture<Duration> overriddenDelayByFuture = new CompletableFuture<>();
    final CompletableFuture<Duration> overriddenIntervalFuture = new CompletableFuture<>();

    @Inject
    ScheduledBean scheduledBean;
    AtomicInteger noConcurrentContenderCnt = new AtomicInteger(0);
    AtomicInteger concurrentContenderCnt = new AtomicInteger(0);

    @Scheduling.FixedRate("PT0.1S")
    public void rate() {
        fixedRateLatch.countDown();
    }

    @Scheduling.FixedRate("PT999999S")
    public void rateFromConfig() {
        fixedRateFromConfigLatch.countDown();
    }

    @Scheduling.Cron("${test-cron-expr}")
    void placeholder() {
        exprLatch.countDown();
    }

    @Scheduling.Cron(value = "0/1 * * * * ? *", concurrent = false)
    void noConcurrentExecutions() throws InterruptedException {
        noConcurrentContenderCnt.incrementAndGet();
        Thread.sleep(1800);
        noConcurrentExecFuture.complete(noConcurrentContenderCnt.get());
        noConcurrentContenderCnt.decrementAndGet();
    }

    @Scheduling.Cron("0/1 * * * * ? *")
    void concurrentExecutions() throws InterruptedException {
        concurrentContenderCnt.incrementAndGet();
        Thread.sleep(1800);
        concurrentExecFuture.complete(concurrentContenderCnt.get());
        concurrentContenderCnt.decrementAndGet();
    }

    @Scheduling.Cron("${overrides.cron:0 0 * * * ? *}")
    void overriddenValuesCron(CronInvocation inv) {
        overriddenCronFuture.complete(inv.cron());
    }

    @Scheduling.FixedRate(delayBy = "${overrides.fixed.delay-by:PT500H}", value = "${overrides.fixed.interval:PT1H}")
    void overriddenValuesFixed(FixedRateInvocation inv) {
        overriddenDelayByFuture.complete(inv.delayBy());
        overriddenIntervalFuture.complete(inv.interval());
    }

    @Test
    void expressionPlaceHolder() throws InterruptedException {
        assertThat("Scheduled method expected to be invoked at least once",
                   exprLatch.await(5, TimeUnit.SECONDS));
    }

    @Test
    void executedEvery2Sec() throws InterruptedException {
        assertThat("Scheduled method expected to be invoked at least twice",
                   scheduledBean.getCountDownLatch().await(5, TimeUnit.SECONDS));
        assertDuration(TWO_SEC_MILLIS, scheduledBean.getDurations(), TWO_SEC_MILLIS);
    }

    @Test
    void vetoedShouldNotBeInvoked() throws InterruptedException {
        Thread.sleep(3000);
        assertFalse(ScheduledVetoMeBean.triggered);
    }

    @Test
    void fixedRate() throws InterruptedException {
        assertThat("Scheduled method expected to be invoked at least 5 times",
                   fixedRateLatch.await(3, TimeUnit.SECONDS));
    }

    @Test
    void fixedRateFromConfig() throws InterruptedException {
        assertThat("Scheduled method expected to be invoked at least twice",
                   fixedRateLatch.await(3, TimeUnit.SECONDS));
    }

    @Test
    void forbiddenConcurrentExec() throws InterruptedException, TimeoutException, ExecutionException {
        assertThat("Scheduled method expected is NOT expected to be invoked concurrently with concurrentExecution = false",
                   noConcurrentExecFuture.get(3, TimeUnit.SECONDS), equalTo(1));
    }

    @Test
    void concurrentExec() throws InterruptedException, TimeoutException, ExecutionException {
        assertThat("Scheduled method is expected to be invoked concurrently with concurrentExecution = true.",
                   concurrentExecFuture.get(3, TimeUnit.SECONDS), greaterThan(1));
    }

    @Test
    void overriddenCron() throws InterruptedException, TimeoutException, ExecutionException {
        assertThat("Cron expression should have been overridden by config value",
                   overriddenCronFuture.get(5, TimeUnit.SECONDS), equalTo("0/2 * * * * ? *"));
    }

    @Test
    void overriddenFixedRate() throws InterruptedException, TimeoutException, ExecutionException {
        Duration delayBy = overriddenDelayByFuture.get(5, TimeUnit.SECONDS);
        Duration interval = overriddenIntervalFuture.get(5, TimeUnit.SECONDS);

        Assertions.assertAll(
                () -> assertThat("Delay by should have been overridden by config value",
                                 delayBy, is(Duration.ofSeconds(1))),
                () -> assertThat("Interval should have been overridden by config value",
                                 interval, is(Duration.ofSeconds(2)))
        );
    }

    private void assertDuration(long expectedDuration, List<Duration> durations, long allowedDiscrepancy) {
        var duration = durations.getFirst().toMillis();
        String durationString = "Expected duration is " + expectedDuration + " mls, but was " + duration + " mls";
        assertThat(durationString, duration, greaterThan(expectedDuration - allowedDiscrepancy));
        assertThat(durationString, duration, lessThan(expectedDuration + allowedDiscrepancy));
    }

    @ApplicationScoped
    public static class ScheduledBean {

        private static final System.Logger LOGGER = System.getLogger(ScheduledBean.class.getName());

        final CountDownLatch countDownLatch = new CountDownLatch(2);

        private final List<Duration> durations = new CopyOnWriteArrayList<>();

        volatile long duration = 0;
        volatile long stamp = 0;

        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }

        public List<Duration> getDurations() {
            return durations;
        }

        @Scheduling.Cron("0/2 * * * * ? *")
        public void test2sec() {
            if(stamp != 0) {
                duration = System.currentTimeMillis() - stamp;
                durations.add(Duration.ofMillis(duration));
            }
            stamp = System.currentTimeMillis();
            LOGGER.log(System.Logger.Level.DEBUG, () -> Thread.currentThread().getName() + " Executed at " + Instant.ofEpochMilli(stamp) + "(" + stamp);
            countDownLatch.countDown();
        }
    }

    @ApplicationScoped
    public static class ScheduledVetoMeBean {

        private static final System.Logger LOGGER = System.getLogger(ScheduledVetoMeBean.class.getName());

        static volatile boolean triggered = false;

        @Scheduling.Cron("0/1 * * * * ? *")
        public void test1sec() {
            triggered = true;
            LOGGER.log(System.Logger.Level.DEBUG, () -> "Executed at " + LocalTime.now());
        }
    }
}
